問題解説: Packet Filtering 筆記

1問目

問題文

あなたはWebサービスを作っています。

このサーバーではnginxが0.0.0.0:80で通信待ち受けしています。このnginxを用いてlocalhost:8080で動いているサービスにproxyするようにしたいのですが、何故か正常に動作しません。

原因を特定し、正常に動作するために必要な手順を説明してください。

なお、nginxは適切に設定がなされているものとします。また、iptablesのポリシーを変更するのは禁止します。

root@ubuntu:/etc/nginx/sites-enabled# systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
   Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
   Active: active (running) since Sun 2018-07-29 11:48:05 JST; 18min ago
     Docs: man:nginx(8)
  Process: 1394 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
  Process: 1393 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
 Main PID: 1395 (nginx)
    Tasks: 3 (limit: 2327)
   CGroup: /system.slice/nginx.service
           ├─1395 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
           ├─1396 nginx: worker process
           └─1397 nginx: worker process
root@ubuntu:/etc/nginx/sites-enabled# ps ax | grep 8080
 2750 pts/0    S      0:00 python3 -m http.server 8080
 2762 pts/0    S+     0:00 grep --color=auto 8080
root@ubuntu:/etc/nginx/sites-enabled# iptables -t filter -L -v
Chain INPUT (policy DROP 46 packets, 4344 bytes)
 pkts bytes target     prot opt in     out     source               destination         
 1220 89088 ACCEPT     tcp  --  any    any     anywhere             anywhere             tcp dpt:ssh
    0     0 ACCEPT     tcp  --  any    any     anywhere             anywhere             tcp dpt:http
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain OUTPUT (policy ACCEPT 772 packets, 90484 bytes)
 pkts bytes target     prot opt in     out     source               destination         
root@ubuntu:/etc/nginx/sites-enabled# iptables -t nat -L -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
root@ubuntu:/etc/nginx/sites-enabled# iptables -t mangle -L -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
root@ubuntu:/etc/nginx/sites-enabled# iptables -t raw -L -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination    

トラブルの概要

nginxとPythonで作成されたWebアプリケーション(以下アプリケーション)がうまく通信できていない。

解説

リクエストが処理される流れを前から追って問題を調べてみましょう。

  • Serverに80/tcpのパケットが送信される。

iptablesのfilterテーブルのINPUTチェインを見るとhttpはACCEPTされているので問題ありません。

  • Linuxからnginxにデータが渡される
  • nginxがlocalhost:8080にproxyする

「nginxは適切に設定されている」とあるので問題ありません。

  • nginxがアプリケーションにリクエストを投げる
  • Serverに8080/tcpのパケットが送信される(内部で)

iptablesのfilterテーブルのINPUTチェインを見ても8080/tcpはACCEPTされていないので問題があります。

  • アプリケーションがnginxにレスポンスを返す
  • nginxにデータが送り返される

nginxがアプリケーションにデータを送る際に、nginxはephemeralなポートを使ってアプリケーションと通信します。アプリケーションはnginxにデータを送り返しますが、iptablesのfilterテーブルのINPUTチェインを見てもephemeralなポートはACCEPTされていないので問題があります。

  • nginxがレスポンスを返す

iptablesのfilterテーブルのOUTPUTチェインを見ても何もルールがないので問題ありません。

これまでの考察を整理すると、nginxとアプリケーションがiptablesの設定により正常に通信できていないことが分かります。

解答例

iptables -A INPUT -p tcp --dport 8080 -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED -j ACCEPT

もしくは

iptables -A INPUT -i lo -j ACCEPT

採点基準

200 OKが返ってくるような設定ならば100点を与えています。
ESTABLISHEDのルールが抜けている場合は50点を与えています。

講評

簡単に点数が取れるだろうと思っていたのですが思ったよりも解けているチームは多くなかったです。

当初、8080/tcpとESTABLISHEDをACCEPTする答えが来ると想定していましたが、loをACCEPTする解答が大半を占めていました。

ちなみに、8080/tcpをACCEPTする解答を送信してきたチームはすべてESTABLISHEDをACCEPTするのを忘れていました。ESTABLISHEDは忘れやすいですし、TCPのESTABLISHEDとは無関係なので、少し厄介なのかなと思います。

2,3問目

問題文

ここにGoで書かれたコードがあります。このプログラムはIPv4のIPアドレスをキーにして通信を破棄することができます。
このコードを参考に、後述する問いに答えてください。

package main

import (
    "fmt"
    "os"

    "github.com/AkihiroSuda/go-netfilter-queue"
    "github.com/google/gopacket/layers"
)

const NFQUEUE_NUM_ID = 10
const MAX_PACKET_IN_QUEUE = 300

const EXCLUDE_IN_IP = "192.168.0.2"
const EXCLUDE_IN_Port = <1>

func isSelectedExcludeInIP(packet *netfilter.NFPacket, target string) {
    if target == EXCLUDE_IN_IP {
        packet.SetVerdict(netfilter.NF_DROP)
        fmt.Println("Drop is IP")
    }
}
func isSelectedExcludeInPort(packet *netfilter.NFPacket, target string) {
    if target == EXCLUDE_IN_Port {
        packet.SetVerdict(netfilter.NF_DROP)
        fmt.Println("Drop is Port")
    }
}

func main() {
    var err error

    nfq, err := netfilter.NewNFQueue(NFQUEUE_NUM_ID, MAX_PACKET_IN_QUEUE, netfilter.NF_DEFAULT_PACKET_SIZE)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer nfq.Close()
    packets := nfq.GetPackets()
    for true {
        select {
        case packet := <-packets:
            ethernetLayer := packet.Packet.Layer(layers.LayerTypeEthernet)
            ipLayer := packet.Packet.Layer(layers.LayerTypeIPv4)
            tcpLayer := packet.Packet.Layer(layers.LayerTypeTCP)
            if ipLayer != nil {
                ip, _ := ipLayer.(*layers.IPv4)
                isSelectedExcludeInIP(&packet, ip.SrcIP.String())
            } else if <2>{
                <3>
            }else {
                packet.SetVerdict(netfilter.NF_ACCEPT)
            }
        }
    }
}

1.このプログラムを用いてパケットを制御することができます。
go run ファイル名 でプログラムを起動し、パケットの制御を行えるのですが、それに加えてiptablesでqueueの設定をしなくてはいけません。
前述したコードを用いてIP通信を制御する際にはどのようなコマンドでqueueの設定をすればよいでしょうか。
コマンドはiptablesから始めて記述してください。
また、startだけではなく、stopの時も空行を挟んで同様に示してください。

2.突然ですがあなたは学内ネットワークの管理者になりました。
そこではsshなんて通しちゃダメという謎の言葉があるドキュメントがあり、あなたはそれに従う必要が出てきました。
前述したコードには<1>,<2>,<3>という穴があります。
その部分を選択肢から一つ選んで埋めてください。

  • A
    • <1>:22
    • <2>:tcpLayer != nil
    • <3>:isSelecteExcludedInMACAddr(&packet, ethernetPacket.SrcMAC.String())
  • B
    • <1>:80
    • <2>:ethernetLayer != nil
    • <3>:isSelecteExcludedInMACAddr(&packet, ethernetPacket.SrcMAC.String())
  • C
    • <1>:23
    • <2>:tcpLayer != nil
    • <3>:isSelectedExcludeInIP(&packet, ip.SrcIP.String())
  • D
    • <1>:22
    • <2>:tcpLayer != nil
    • <3>:isSelectedExcludeInPort(&packet, tcp.SrcPort.String())

概要

  • netfilter queueの実行の仕方
  • sshのブロックを行う方法

解説

2問目

queueというのはiptablesのnetfilterでユーザー側にデータを渡すための待ち行列のことで、queue番号というのはその待ち行列に対するタグ付けした番号です。
この問題のキモとして、入力としてQUEUE番号を何に指定しているのかということを理解している必要があります。
この場合はconst NFQUEUE_NUM_ID = 10 より10にタグ付けされているのでNFQUEUE --queue-num 10ということになります。

3問目

  • <1>
    SSHのデフォルトポートである22番ポートを指定する
  • <2>
    TCPのコネクションが確立されているかの判定を行う。tcpLayer!=nilならば、それはTCPのレイヤーでの通信挙動を取得できているということになる。
  • <3>
    TCPのデータを抽出し、それをチェックする関数を呼び出す。

解答例

2問目

A.

//start: sudo iptables -A OUTPUT -j NFQUEUE --queue-num 10
//end  : sudo iptables -D OUTPUT -j NFQUEUE --queue-num 10

3問目

  • <1>
    22
  • <2>
    tcpLayer != nil
  • <3>
tcp, _ := tcpLayer.(*layers.TCP)
isSelectedExcludeInPort(&amp;packet, tcp.SrcPort.String())

採点基準

2問目

  • queue番号を10ということを言及できている 30点
  • 実行と解除のコマンドを示すことができている 30点
  • きちんと実行できる 40点

3問目

正答であれば満点

講評

2問目では誘導でキーワードを調べる機会として、
3問目では実際にコードに触れ、どういう挙動になるのか簡単に知ってもらうという意図からの問題でした。
3問目は選択式ということもあり正答率が半数を超えて高く、点数源にしてもらえていてよかったです。しかしながら2問目は解いているチームが半数ぐらいで驚きました。

4,5問目

問題文

4問目

ネットワークのパケットをtcpdumpでキャプチャするときの基礎として「フィルタリングの条件式構文」が挙げられます。
式は一つかそれ以上の要素で構成され、要素は通常は一つかそれ以上の修飾子によって構成されています。

これらの具体例を挙げると、
– 192.168.0.1の場合だけを見たい場合はtcpdump net 192.168.0.1
– ポート21~23だけを見たい場合はtcpdump portrange 21-23

のようになり、任意の条件式を使うことができます。

この前提から、以下の問題に答えてください。

  • tcpdumpでinterface eth1から取得して、HTTP GETとPOSTのみを表示するという条件でフィルタリングして表示してください。
  • また、tcpdumpでinterface eth0から取得して、宛先アドレスは192.168.2.200または192.168.1.100、TCPでポート番号80という条件でフィルタリングを行い表示してください。

5問目

フィルタリングの技術としてBPF(Berkeley Packet Filter)というものがあります。
BPFはレジスタマシーンとして命令を受理して動作が行われます。 これを踏まえると
tcpdumpはフィルタ条件式のパーサーとそれをコンパイルすることのできるある種のドメイン言語ともいうことができ、
最近ではJITコンパイルを行うものも存在します。
tcpdumpがコンパイラと同じということは、パースしたフィルタ条件式から変換されたバイトコードを吐くという事です。

例えば
tcpdump -p -ni en0 -d "ip and udp"
を実行すると以下のようなバイトコードが出力されます。

(000) ldh      [12]
(001) jeq      #0x800           jt 2    jf 5
(002) ldb      [23]
(003) jeq      #0x11            jt 4    jf 5
(004) ret      #262144
(005) ret      #0

軽く説明すると、0x0800がイーサネットフレーム上のIPv4を示していて、0x11がUDPということを示しています。
これらがマッチした場合に最後の004に飛んで無事フィルターにマッチしたものが出力されるという動作をします。
※本文では005と書きましたが正しくは004です

上記の前提から、以下のバイトコードの示している条件式を答えてください。

(000) ldh      [12]
(001) jeq      #0x86dd          jt 16 jf 2
(002) jeq      #0x800           jt 3  jf 16
(003) ldb      [23]
(004) jeq      #0x6             jt 5  jf 16
(005) ldh      [20]
(006) jset     #0x1fff          jt 16 jf 7
(007) ldxb     4*([14]&amp;0xf)
(008) ldh      [x + 14]
(009) jeq      #0x50            jt 12 jf 10
(010) ldh      [x + 16]
(011) jeq      #0x50            jt 12 jf 16
(012) ld       [30]
(013) jeq      #0xc0a802c8      jt 15 jf 14
(014) jeq      #0xc0a80164      jt 15 jf 16
(015) ret      #262144
(016) ret      #0

トラブルの概要

  • パケットフィルタリングを行うのに行うべき条件を正しく理解して表現できるか

解説

4問目

1.tcpdump -i eth1 'tcp [32:4] = 0x47455420 or tcp[32:4] = 0x504f5354'
が答えです(一例)

tcpdump -i eth1でinterface eth1から取得しています。

次にtcp[32:4]が何を示しているかについて説明します。
これはTCPヘッダーの32バイトから4バイトの参照ということを示しています。
この参照先ではTCPヘッダーのうち、オプションを含めない領域の20バイトに加えてTCPのコネクションが成立している時に付くオプションのことを指しています。

主にTCPは以下のオプションパラメータがあります。
– mss : 最大セグメント長 Max-segment-size
– wscale : ウィンドウスケール
– sackOK : Selective Acknowledgmentが有効
– TS val ecr : valはタイムスタンプ ecrはecho reply 参照 rfc7323
– nop : パディング No operation provides padding around other options
– eol : オプションリストの終わり

options [nop,nop,TS val ecr ]

というような想定されるパターンが帰ってくる時には、一般的にオプションは12バイトということが一般に知られています。
ということでTCPヘッダーのオプションを含めないサイズ20バイト+オプションで12バイトということで32バイトとなります。

そこからHTTPリクエストは、次のように開始されます。例としてあげたこれの先頭にGETという文字列が存在します。これは先頭の3バイトについてあります。

GET / HTTP / 1.1 \ r \ n

これをもとにGETとPOSTのみを表示すると言う条件でフィルタリングについて考えると

&gt;&gt;&gt; import binascii
&gt;&gt;&gt; binascii.hexlify(b&#039;GET&#039;) 
b&#039;474554&#039;
&gt;&gt;&gt; binascii.hexlify(b&#039;POST&#039;) 
b&#039;504f5354&#039;

ということがわかりました。またなぜ「4」という指定なのかというのは試しに他の3などを入れるとわかるのですが

tcpdump: data size must be 1, 2, or 4

というエラーが出力される事が物語っているので興味のある人は理由を考えてみると面白いでしょう。

2.tcpdump -i eth0 '((tcp) and (port 80) and ((dst host 192.168.2.200) or (dst host 192.168.1.100)))'が答えです(一例)

-i eth0 でインターフェイスとしてeth0を指定しています。
'((tcp) and (port 80) and ((dst host 192.168.2.200) or (dst host 192.168.1.100)))'ではtcpport 80のものでかつ宛先IPが192.168.2.200192.168.1.100のどちらかであるものという条件式になっています。

5問目

((tcp) and (port 80) and ((dst host 192.168.2.200) or (dst host 192.168.1.100)))
が答えなのですが皆さんは解けましたでしょうか。根気よく調べながら追っていけば簡単に解ける問題だったと思います。

バイトコードは以下の方法で確認できます。(環境によってinterfaceは変えてください。)
sudo tcpdump -p -ni en0 -d '((tcp) and (port 80) and ((dst host 192.168.2.200) or (dst host 192.168.1.100)))'

では簡単にですが実際にバイトコードを追ってみましょう。

(000) ldh      [12]
(001) jeq      #0x86dd          jt 16   jf 2
(002) jeq      #0x800           jt 3    jf 16

(000)でオフセットのロードをした後、
(001)と(002)ではIPv4とIPv6であることを確認しています。
jeqなのでマッチしたら002に飛ぶという動きをしています。
これでTCPである前提条件のIPが使われているというがわかりました。

(003) ldb      [23]
(004) jeq      #0x6             jt 5    jf 16

(003)で 12バイトのオフセットのロードをした後、
(004)で次のヘッダーが6であるかのチェックをしています.
これは6==(tcp) ということがわかります。

(005) ldh      [20]
(006) jset     #0x1fff          jt 16   jf 7
(007) ldxb     4*([14]&amp;0xf)
(008) ldh      [x + 14]
(009) jeq      #0x50            jt 12   jf 10
(010) ldh      [x + 16]
(011) jeq      #0x50            jt 12   jf 16

(005)は(flags + frag offset)というサイズのロードをしていて
(006)は0x1fff == 0001 1111 1111 1111 というフラグメントオフセットが真かどうかのマッチをしています。

(007)はかなり厳つい感じですが
x == 4*ipヘッダの長さということを示しています。つまりTCPヘッターの基本サイズです
なのでx = 20バイトと考えてあげることができます。
(008)ではハーフワードをパケットオフセットx + 14にロードしています。この場合はパケットオフセットが20のため、20+14 で 34になります。
(009)からは、上でパケットオフセットの位置を整えたため単純なマッチで(途中にロードを含んでいますが)ポート番号のチェックをしています。
ここでは0x50番ポート、つまり80番ポートであることのチェックをしています。

(012) ld       [30]
(013) jeq      #0xc0a802c8      jt 15   jf 14
(014) jeq      #0xc0a80164      jt 15   jf 16
(015) ret      #262144
(016) ret      #0

(012)で30をロードしていますが、これは宛先アドレスへの移動をしています。
その先の(013),(014)で宛先アドレスの判定を行っています。
この16進数がIPアドレスを示しており、以下のように変換されます。
0xc0a802c8->192.168.2.200
0xc0a801640->192.168.1.100

最後に、前述までのバイトコードで頻繁に出てきた16は失敗した時に送られるnon-matchを返すコードのアドレス(016)で、最終的に成功した時は15というmatchを返すコードのアドレス(015)です。
条件式の結果により、どちらかのアドレスにジャンプすることでパケットの分類が完了します。

というような手順でパケットを読み解くための条件式を導き出すことができます。

解答例

4問目

1.tcpdump -i eth1 'tcp [32:4] = 0x47455420 or tcp[32:4] = 0x504f5354'
2.tcpdump -i eth0 '((tcp) and (port 80) and ((dst host 192.168.2.200) or (dst host 192.168.1.100)))'

5問目

((tcp) and (port 80) and ((dst host 192.168.2.200) or (dst host 192.168.1.100)))

採点基準

4問目

  • 一つ目はPOSTとGETを示したら10+10点, 全て示して動いたら30点
  • 二つ目は全て示して50点

で合わせて100点

5問目

  • IPv4 IPv6ということを読み解き言及している 50点
  • Port 80ということを読み解き言及している 50点
  • Destination Hostについて言及している 50点
  • 上記を満たした上で実行して動くということが完全に示せている 50点

講評

まずは問題文の訂正です。

これらがマッチした場合に最後の005に飛んで無事フィルターにマッチしたものが出力されるという動作をします。

と問題文の簡単な説明の時に書きましたが、正しくは004です。
間接的にですが間違った言及をしたことをお詫び申し上げます。

4問目ではどういうシンタックスなのかを調べる機会として、
5問目では本質的にどういう挙動になるのか簡単に触れてもらうという意図で作成した問題でした。

4問目の2つ目は比較的簡単に作ったので得点源に・・・と思っていましたが、実際に出題されると意外なことに4問目の1つ目が割と解かれていた印象でした。
個人的に面白かった点として、どこから参考にしたのか基本的に問題を解いた方は判を押したように tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420' and ~~~ のような記法で書いていた方がほとんどでした。
気になって調べたところ、某緑色の技術記事を投稿するサイトが日本語記事でトップに出てくるようで、そちらと同じ記法であることから、そのサイトを参考にしたのだろうと思いながら採点を行いました。

5問目は一見点数を取らせないような問題に見せていますが、実はよく読めば解ける問題です。解けるだろうと前述した「4問目の2つ目」がそのまま答えでした。
条件が複雑なものが全問に記載されていたこともあり、未回答も目立ちましたが割とアプローチをした跡があった事から参加者たちのレベルの高さが伺えます。
やはりきちんと解析をしてみるというのは大切だと感じました。これはいくつかのチームが解いてくれていましたが満点を取ったのは2チームだけでした(拍手)

ぜひ自分のチームは5問目にアプローチしたなーと思った方は回答に合わせてtcpdumpに-dをつけて挙動を見てみてくださいね!